<?php

namespace Wgx\Admin\Com\Crud;

use Wgx\Admin\Com\AdminMenuRightsUtil;
use Wgx\Admin\Com\NoMenuRightException;
use Wgx\Admin\Entity\AdminToMenuEntity;
use PhpYes\Apps;
use PhpYes\Com\HttpException;
use PhpYes\Entity\BaseEntity;
use Wgx\PdoEx\QueryBuilder;

/**
 * Class Crud
 * op GET:{listpage,listdata,data,readpage,editpage,export}, POST|PUT:{create:POST|PUT默认id=0区分,update:POST|PUT默认id>0区分,xxupdate:自定义update,...}, DELETE:{delete:DELETE默认}
 * create(editpage,data,[POST|PUT]),
 * read(readpage,data),
 * update(editpage,data,[POST|PUT]),
 * delete([DELETE]),
 *
 * 设计 列表页(listpage) 的数据使用前端渲染(CrudField->lpRender),
 * 所以后端在给出 列表页(listpage) 的数据的时候, 使用 CrudUserInterface::formatListData() 给出的数据即可,
 * 另外, 后端在给出 列表页(listpage) 的数据的时候, 字段数量应该满足前端渲染(CrudField->lpRender)和页面逻辑的需求,
 * 同时又要避免给出大量的无用字段,
 * 和避免给出不应该给的敏感数据字段,
 * 这就要求好好设计使用 Crud::addField 添加到 Crud 中的字段了.
 *
 * 设计 查看页(readpage) 的数据使用后端渲染,
 * 所以后端在给出 查看页(readpage) 的数据的时候, 使用 CrudUserInterface::formatData() 给出数据, 给出的字段的值可以是 html 代码片段作为后端渲染
 * 另外, 后端在给出 查看页(readpage) 的数据的时候, 只需要给出那些需要渲染出的字段的数据即可.
 *
 * 设计 编辑页(editpage) 的数据使用后端渲染,
 * 所以后端在给出 编辑页(editpage) 的数据的时候, 使用 CrudUserInterface::formatData() 给出数据, 给出的字段的值可以是 html 代码片段作为后端渲染
 * 另外, 后端在给出 编辑页(editpage) 的数据的时候, 只需要给出那些需要渲染出的字段的数据即可.
 *
 * 设计 导出(export) 的数据使用后端渲染,
 * 所以后端在给出 导出(export) 的数据的时候, 使用 CrudUserInterface::formatData() 给出数据, 给出的字段的值可以是 html 代码片段作为后端渲染
 * 另外, 后端在给出 导出(export) 的数据的时候, 只需要给出那些需要导出的字段的数据即可.
 *
 * @package Wgx\Admin\Com\Crud
 */
class Crud
{
    const FileCssInline = 'cssInline';
    const FileCssFile = 'cssFile';
    const FileJsInline = 'jsInline';
    const FileJsFile = 'jsFile';

    const PageListPage = 'listpage';
    const PageReadPage = 'readpage';
    const PageEditPage = 'editpage';

    /** @var BaseEntity */
    private $entityTpl;
    /** @var CrudUserInterface */
    private $crudUser;

    /** @var string|null */
    private $op;
    /** @var string|null */
    private $opName;

    /**
     * 从数据库 select 的字段
     * 目前, op = listdata|export 时起作用
     * 使用场景一般是 数据表中有大数据字段, 但列表页不需要展示这些大数据字段, 所以为了性能, 做数据库查询时最好按需取字段
     * '*' 或者 'col1,col2,...' 或者 ['col1', 'col2', ...], 默认 *
     * @var string|string[]
     */
    private $columns;

    /** @var array */
    private $fieldMap;

    /** @var array */
    private $fileMap;

    private $lpOrderFieldName = 'id';
    private $lpOrderDirection = 'asc';

    /** @var array */
    private $lpSearchFieldMap;

    /** @var array */
    private $pageDisplayMap;

    /**
     * Crud constructor.
     * @param BaseEntity $entityTpl
     * @param CrudUserInterface $crudUser
     */
    public function __construct(BaseEntity $entityTpl, CrudUserInterface $crudUser)
    {
        $this->entityTpl = $entityTpl;
        $this->crudUser = $crudUser;
        $this->fieldMap = array();
    }

    /**
     * @return BaseEntity
     */
    public function getEntityTpl()
    {
        return $this->entityTpl;
    }

    /**
     * @return string|null
     */
    public function getOp()
    {
        return $this->op;
    }

    /**
     * @return string|null
     */
    public function getOpName()
    {
        return $this->opName;
    }

    /**
     * '*' 或者 'col1,col2,...' 或者 ['col1', 'col2', ...], 默认 *
     * @return null|string|string[]
     */
    public function getColumns()
    {
        return $this->columns;
    }

    /**
     * 从数据库 select 的字段
     * 目前, op = listdata|export 时起作用
     * 使用场景一般是 数据表中有大数据字段, 但列表页不需要展示这些大数据字段, 所以为了性能, 做数据库查询时最好按需取字段
     * @param string|string[] $columns - '*' 或者 'col1,col2,...' 或者 ['col1', 'col2', ...], 默认 *
     */
    public function setColumns($columns)
    {
        $this->columns = $columns;
    }

    /**
     * [fieldName1 => CrudField1, fieldName2 => CrudField2, ...]
     * @return array
     */
    public function getFieldMap()
    {
        return $this->fieldMap;
    }

    /**
     * @param string $fieldName
     * @return CrudField|null
     */
    public function getField($fieldName)
    {
        $fieldName = empty($fieldName) ? null : trim($fieldName);
        if ($fieldName && $this->fieldMap) {
            return empty($this->fieldMap[$fieldName]) ? null : $this->fieldMap[$fieldName];
        }
        return null;
    }

    /**
     * @param CrudField $field
     * @return $this
     */
    public function addField(CrudField $field)
    {
        if ($field) {
            $fieldName = $field->getName();
            $fieldName = empty($fieldName) ? null : trim($fieldName);
            if ($fieldName) {
                $this->fieldMap[$fieldName] = $field;
            }
        }
        return $this;
    }

    /**
     * @param string $fieldName
     * @return $this
     */
    public function delField($fieldName)
    {
        $fieldName = empty($fieldName) ? null : trim($fieldName);
        if ($fieldName && $this->fieldMap) {
            unset($this->fieldMap[$fieldName]);
        }
        return $this;
    }

    /**
     * 获取页面的资源文件
     * @param string $pageType - refer self::PageXX
     * @param string $fileType - refer self::FileXX
     * @return array|null - [file1 => true, file2 => true, ...]
     */
    public function getFileMap($pageType, $fileType)
    {
        $pageType = empty($pageType) ? '' : trim($pageType);
        $fileType = empty($fileType) ? '' : trim($fileType);
        return empty($this->fileMap[$pageType][$fileType]) ? null : $this->fileMap[$pageType][$fileType];
    }

    /**
     * 给页面添加资源文件
     * 文件类型为 inline(即 $fileType 为 self::FileCssInline|self::FileJsInline) 的资源文件将采用 php require 内联加载到页面上
     * self::FileCssInline 类型的资源文件将采用 php require 内联加载到页面上 <style> 标签中
     * self::FileJsInline 类型的资源文件将采用 php require 内联加载到页面上 <script> 标签中
     * self::FileCssFile 类型的资源文件将由页面的 <link rel="stylesheet" href="{{file}}" /> 进行加载
     * self::FileJsFile 类型的资源文件将由页面的 <script src="{{file}}"></script> 进行加载
     * @param string $pageType - refer self::PageXX
     * @param string $fileType - refer self::FileXX
     * @param string $file - 要添加的文件
     * @return $this
     */
    public function addFile($pageType, $fileType, $file)
    {
        $pageType = empty($pageType) ? '' : trim($pageType);
        $fileType = empty($fileType) ? '' : trim($fileType);
        $file = empty($file) ? null : trim($file);

        if ($file) {
            if (empty($this->fileMap)) {
                $this->fileMap = array();
            }
            if ($fileType == self::FileCssInline || $fileType == self::FileJsInline) {
                if ($pageType == self::PageListPage
                    || $pageType == self::PageReadPage
                    || $pageType == self::PageEditPage
                ) {
                    $file = realpath($file);
                    if ($file) {
                        $this->fileMap[$pageType][$fileType][$file] = true;
                    }
                }
            } elseif ($fileType == self::FileCssFile || $fileType == self::FileJsFile) {
                if ($pageType == self::PageListPage
                    || $pageType == self::PageReadPage
                    || $pageType == self::PageEditPage
                ) {
                    $this->fileMap[$pageType][$fileType][$file] = true;
                }
            }
        }

        return $this;
    }

    /**
     * 删除已经添加给页面的资源文件
     * @param string $pageType - refer self::PageXX
     * @param string $fileType - refer self::FileXX
     * @param string $file - 要删除的文件
     * @return $this
     */
    public function delFile($pageType, $fileType, $file)
    {
        $pageType = empty($pageType) ? '' : trim($pageType);
        $fileType = empty($fileType) ? '' : trim($fileType);
        $file = empty($file) ? null : trim($file);

        if ($file) {
            if ($fileType == self::FileCssInline || $fileType == self::FileJsInline) {
                $file = realpath($file);
                if ($file && !empty($this->fileMap[$pageType][$fileType][$file])) {
                    unset($this->fileMap[$pageType][$fileType][$file]);
                }
            } else {
                if (!empty($this->fileMap[$pageType][$fileType][$file])) {
                    unset($this->fileMap[$pageType][$fileType][$file]);
                }
            }
        }

        return $this;
    }

    /**
     * 设置 列表页(listpage) 的默认排序
     * @param string $fieldName
     * @param string $direction - {asc, desc}
     */
    public function setLpOrder($fieldName, $direction)
    {
        $this->lpOrderFieldName = $fieldName;
        if (empty($direction) || $direction == 'desc') {
            $this->lpOrderDirection = 'desc';
        } else {
            $this->lpOrderDirection = 'asc';
        }
    }

    /**
     * 获取 列表页(listpage) 的默认排序字段名
     * @return string
     */
    public function getLpOrderFieldName()
    {
        return $this->lpOrderFieldName;
    }

    /**
     * 获取 列表页(listpage) 的默认排序方向 {asc, desc}
     * @return string
     */
    public function getLpOrderDirection()
    {
        if (empty($this->lpOrderDirection) || $this->lpOrderDirection == 'desc') {
            return 'desc';
        } else {
            return 'asc';
        }
    }

    /**
     * [fieldName1 => ListPageSearchInputBase1, fieldName2 => ListPageSearchInputBase2, ...]
     * @return array
     */
    public function getLpSearchFieldMap()
    {
        return $this->lpSearchFieldMap;
    }

    /**
     * @param string $fieldName
     * @param ListPageSearchInputBase $inputWidget
     * @return $this
     */
    public function addLpSearchField($fieldName, ListPageSearchInputBase $inputWidget)
    {
        $fieldName = empty($fieldName) ? null : trim($fieldName);
        if ($fieldName) {
            if (empty($this->lpSearchFieldMap)) {
                $this->lpSearchFieldMap = array();
            }
            $this->lpSearchFieldMap[$fieldName] = $inputWidget;
        }
        return $this;
    }

    /**
     * @param string $fieldName
     * @return $this
     */
    public function delLpSearchField($fieldName)
    {
        $fieldName = empty($fieldName) ? null : trim($fieldName);
        if ($fieldName && $this->lpSearchFieldMap) {
            unset($this->lpSearchFieldMap[$fieldName]);
        }
        return $this;
    }

    /**
     * @param string $pageType - enum{self::PageReadPage, self::PageEditPage}
     * @param string $target - enum{_self, _blank, layer}
     * @param int|string|null $width - $target=_self|_blank时无用, $target=layer时设置给 layer 的 area 参数的宽
     * @param int|string|null $height - $target=_self|_blank时无用, $target=layer时设置给 layer 的 area 参数的高
     * @return Crud
     */
    public function setPageDisplayTarget($pageType, $target, $width = 0, $height = 0)
    {
        $pageType = empty($pageType) ? '' : trim($pageType);
        $target = empty($target) ? '' : trim($target);
        if (empty($this->pageDisplayMap)) {
            $this->pageDisplayMap = array();
        }
        if ($pageType == self::PageReadPage) {
            if ($target == '_self' || $target == '_blank' || $target == 'layer') {
                $this->pageDisplayMap[$pageType]['target'] = $target;
                $this->pageDisplayMap[$pageType]['width'] = $width;
                $this->pageDisplayMap[$pageType]['height'] = $height;
            }
        } elseif ($pageType == self::PageEditPage) {
            if ($target == '_self' || $target == '_blank' || $target == 'layer') {
                $this->pageDisplayMap[$pageType]['target'] = $target;
                $this->pageDisplayMap[$pageType]['width'] = $width;
                $this->pageDisplayMap[$pageType]['height'] = $height;
            }
        }

        return $this;
    }

    /**
     * @param string $pageType - enum{self::PageReadPage, self::PageEditPage}
     * @return string - enum{_self, _blank, layer}
     */
    public function getPageDisplayTarget($pageType)
    {
        $pageType = empty($pageType) ? null : trim($pageType);
        if (empty($pageType)) {
            return '_blank';
        }

        $target = empty($this->pageDisplayMap[$pageType]['target']) ? null : $this->pageDisplayMap[$pageType]['target'];
        if (empty($target)) {
            return '_blank';
        }
        if ($target == '_self' || $target == '_blank' || $target == 'layer') {
            return $target;
        } else {
            return '_blank';
        }
    }

    /**
     * @param string $pageType - enum{self::PageReadPage, self::PageEditPage}
     * @return int|string|null - $target=_self|_blank时无用, $target=layer时设置给 layer 的 area 参数的宽
     */
    public function getPageDisplayWidth($pageType)
    {
        $pageType = empty($pageType) ? null : trim($pageType);
        if (empty($pageType)) {
            return null;
        }
        return empty($this->pageDisplayMap[$pageType]['width']) ? null : $this->pageDisplayMap[$pageType]['width'];
    }

    /**
     * @param string $pageType - enum{self::PageReadPage, self::PageEditPage}
     * @return int|string|null - $target=_self|_blank时无用, $target=layer时设置给 layer 的 area 参数的高
     */
    public function getPageDisplayHeight($pageType)
    {
        $pageType = empty($pageType) ? null : trim($pageType);
        if (empty($pageType)) {
            return null;
        }
        return empty($this->pageDisplayMap[$pageType]['height']) ? null : $this->pageDisplayMap[$pageType]['height'];
    }

    /**
     * @return string
     * @throws HttpException
     * @throws \Exception
     */
    public function execute()
    {
        $fieldMap = $this->fieldMap;
        if (empty($fieldMap)) {
            throw new \Exception('Crud设置错误，请添加Crud字段');
        }
        $method = App::getReqMethod();
        $rights = AdminMenuRightsUtil::getSingleObj()->getCurrentRights();
        if ($method == 'GET') {
            if ($rights < 1 || ($rights & AdminToMenuEntity::RightRead) < 1) {
                throw new NoMenuRightException();
            }

            $param = \PhpYes\Apps::getReqQueryParams();
            $op = empty($param['_op_']) ? null : trim($param['_op_']);
            if (empty($op)) {
                throw new HttpException('请传递op参数');
            }
            $this->op = $op;

            if ($op == 'listpage') {
                $this->opName = '列表页';
                $this->crudUser->setForOp($this, null);
                return App::render()->display('crud/list.html.php', array('crud' => $this));
            } elseif ($op == 'listdata') {
                // $this->crudUser->setForGetListData($this);

                $where = $this->crudUser->buildWhereForGetListData($param);

                $orderBy = null;
                $orderFieldName = empty($param['_orderFieldName']) ? null : trim($param['_orderFieldName']);
                if ($orderFieldName) {
                    $orderBy = $this->crudUser->buildOrderByForGetListData($orderFieldName, empty($param['_orderDirection']) ? 0 : intval($param['_orderDirection']));
                }

                $entityTpl = $this->entityTpl;
                $qb = QueryBuilder::useDb($entityTpl->getConnName());

                $limit = empty($param['_limit']) ? 10 : intval($param['_limit']);
                $offset = empty($param['_offset']) ? 0 : intval($param['_offset']);

                $columns = $this->columns;
                if (empty($columns)) {
                    $columns = '*';
                } elseif (is_string($columns)) {
                    $columns = trim($columns);
                    if (empty($columns)) {
                        $columns = '*';
                    }
                } elseif (is_array($columns)) {
                    $columns = implode(',', array_map(function ($column) {
                        return trim(strval($column));
                    }, $columns));
                    if (empty($columns)) {
                        $columns = '*';
                    }
                } else {
                    $columns = '*';
                }

                $qbResult = $qb->select($entityTpl->getTable(), "SQL_CALC_FOUND_ROWS {$columns}", $where, null, $orderBy, $limit, $offset);
                $dataList = $qbResult->getPdoStatement()->fetchAll(\PDO::FETCH_ASSOC);
                $result = $qbResult->getPdo()->query('SELECT FOUND_ROWS() AS cnt;')->fetchAll(\PDO::FETCH_ASSOC);
                $total = empty($result[0]['cnt']) ? 0 : intval($result[0]['cnt']);
                if (empty($dataList)) {
                    return App::render()->returnJson(array(
                        'draw' => empty($param['_draw']) ? 0 : intval($param['_draw']),
                        'recordsTotal' => $total,
                        'recordsFiltered' => $total,
                        'data' => array(),
                    ));
                }

                $dataList = $this->crudUser->formatListData($this, $dataList, ['where' => $where, 'orderBy' => $orderBy, 'limit' => $limit, 'offset' => $offset, 'total' => $total]);

                return App::render()->returnJson(array(
                    'draw' => empty($param['_draw']) ? 0 : intval($param['_draw']),
                    'recordsTotal' => $total,
                    'recordsFiltered' => $total,
                    'data' => $dataList,
                ));
            } elseif ($op == 'export') {
                // $this->crudUser->setForGetExport($this);

                $dataList = array();
                $exportFields = array();
                $columnHeads = array();
                /**
                 * @var string $fieldName
                 * @var CrudField $field
                 */
                foreach ($fieldMap as $fieldName => $field) {
                    $showIn = intval($field->getShowIn());
                    if (($showIn & CrudField::ShowInExport) > 0) {
                        $exportFields[] = $fieldName;
                        $columnHeads[] = $field->getHName();
                    }
                }
                if (empty($columnHeads)) {
                    return App::render()->downloadCsv('菜单列表.csv', array());
                }
                $dataList[] = $columnHeads;

                $where = $this->crudUser->buildWhereForGetListData($param);

                $orderBy = null;
                $orderFieldName = empty($param['_orderFieldName']) ? null : trim($param['_orderFieldName']);
                if ($orderFieldName) {
                    $orderBy = $this->crudUser->buildOrderByForGetListData($orderFieldName, empty($param['_orderDirection']) ? 0 : intval($param['_orderDirection']));
                }

                $entityTpl = $this->entityTpl;
                $qb = QueryBuilder::useDb($entityTpl->getConnName());

                $columns = $this->columns;
                if (empty($columns)) {
                    $columns = '*';
                } elseif (is_string($columns)) {
                    $columns = trim($columns);
                    if (empty($columns)) {
                        $columns = '*';
                    }
                } elseif (is_array($columns)) {
                    $columns = implode(',', array_map(function ($column) {
                        return trim(strval($column));
                    }, $columns));
                    if (empty($columns)) {
                        $columns = '*';
                    }
                } else {
                    $columns = '*';
                }

                $result = $qb->select($entityTpl->getTable(), $columns, $where, null, $orderBy)->getPdoStatement()->fetchAll(\PDO::FETCH_ASSOC);
                if (empty($result)) {
                    return App::render()->downloadCsv('菜单列表.csv', $dataList);
                }
                $result = $this->crudUser->formatListData($this, $result, ['where' => $where, 'orderBy' => $orderBy]);
                if ($result) {
                    foreach ($result as $item) {
                        $row = array();
                        foreach ($exportFields as $fieldName) {
                            $row[] = isset($item[$fieldName]) ? $item[$fieldName] : '';
                        }
                        $dataList[] = $row;
                    }
                }

                return App::render()->downloadCsv('菜单列表.csv', $dataList);
            } elseif ($op == 'readpage' || $op == 'editpage' || $op == 'data') {
                $id = empty($param['id']) ? 0 : intval($param['id']);
                if ($op == 'editpage') {
                    if (empty($id)) {
                        $class = get_class($this->entityTpl);
                        $entity = new $class();
                        $this->opName = '新增';
                    } else {
                        $entity = null;
                        if ($id > 0) {
                            $entity = $this->entityTpl->selectById($id);
                        }
                        if (empty($entity)) {
                            throw new HttpException('请传递有效的id参数');
                        }
                        $this->opName = '修改';
                    }
                    $this->crudUser->setForOp($this, ['entity' => $entity]);
                } else {
                    if (empty($id)) {
                        throw new HttpException('请传递id参数');
                    }
                    $entity = null;
                    if ($id > 0) {
                        $entity = $this->entityTpl->selectById($id);
                    }
                    if (empty($entity)) {
                        throw new HttpException('请传递有效的id参数');
                    }

                    if ($op == 'readpage') {
                        $this->opName = '查看';
                        $this->crudUser->setForOp($this, ['entity' => $entity]);
                    }
                    // else {
                    //     $this->crudUser->setForGetData($this, $entity);
                    // }
                }

                $data = $this->crudUser->formatData($this, $entity);

                if ($op == 'readpage') {
                    return App::render()->display('crud/read.html.php', array('crud' => $this, 'data' => $data));
                } elseif ($op == 'editpage') {
                    return App::render()->display('crud/edit.html.php', array('crud' => $this, 'data' => $data));
                } else {
                    return App::render()->returnJson($data);
                }
            } else {
                throw new HttpException('错误的op参数');
            }
        } elseif ($method == 'POST' || $method == 'PUT') {
            $param = \PhpYes\Apps::getReqBodyParams();
            $op = empty($param['_op_']) ? null : trim($param['_op_']);
            $this->op = $op;

            $id = empty($param['id']) ? 0 : intval($param['id']);
            $entity = null;
            if (empty($id)) {
                if ($rights < 1 || ($rights & AdminToMenuEntity::RightCreate) < 1) {
                    throw new NoMenuRightException();
                }
                $class = get_class($this->entityTpl);
                $entity = new $class();
                // $this->crudUser->setForCreate($this, $entity, $param);
            } else {
                if ($rights < 1 || ($rights & AdminToMenuEntity::RightUpdate) < 1) {
                    throw new NoMenuRightException();
                }
                $entity = null;
                if ($id > 0) {
                    $entity = $this->entityTpl->selectById($id);
                }
                if (empty($entity)) {
                    throw new HttpException('请传递有效的id参数');
                }
                // $this->crudUser->setForUpdate($this, $entity, $param);
            }
            // $this->crudUser->setForCreateOrUpdate($this, $entity, $param);
            $this->crudUser->createOrUpdate($this, $entity, $param);

            if ($entity->isNeedUpdateToDb()) {
                $qbResult = $entity->updateToDb();
                if (empty($qbResult) || !$qbResult->getIsOk() || $qbResult->getPdoStatement()->rowCount() < 1) {
                    if (empty($id)) {
                        return App::render()->returnJsonWithErrorMessage('新增失败，请稍候重试');
                    } else {
                        return App::render()->returnJsonWithErrorMessage('修改失败，请稍候重试');
                    }
                }
            }
            return App::render()->returnJsonSuccess();
        } elseif ($method == 'DELETE') {
            if ($rights < 1 || ($rights & AdminToMenuEntity::RightDelete) < 1) {
                throw new NoMenuRightException();
            }
            $param = \PhpYes\Apps::getReqQueryParams();
            $id = empty($param['id']) ? null : trim($param['id']);
            if (empty($id)) {
                throw new HttpException('请传递要删除的数据的id');
            }
            // $this->crudUser->setForDelete($this, $id);
            $ids = explode(',', $id);
            $validIds = array();
            foreach ($ids as $id) {
                $id = empty($id) ? 0 : intval($id);
                if ($id > 0) {
                    $validIds[$id] = 1;
                }
            }
            if ($validIds) {
                $this->crudUser->deleteByIds($this, array_keys($validIds));
            }
            return App::render()->returnJsonSuccess();
        } else {
            throw new HttpException("{$method} 请求不支持!");
        }
    }

}